
/*
 * Copyright (c) 2010 Sergey N Frolov
 * 
 * See LICENSE file
 */

#include "ngx_http_javascript_module.h"

static ngx_conf_post_handler_pt ngx_http_javascript_init_post_p = ngx_http_javascript_init_post;

static ngx_command_t ngx_http_javascript_commands[] = {
	// Enable JavaScript engine
	{ ngx_string("javascript"),
	  NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
	  ngx_conf_set_flag_slot,
	  NGX_HTTP_LOC_CONF_OFFSET,
	  offsetof(ngx_http_javascript_loc_conf_t, enable),
	  &ngx_http_javascript_init_post_p },
	// Basedir for opening files
	{ ngx_string("javascript_basedir"),
	  NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
	  ngx_conf_set_str_slot,
	  NGX_HTTP_LOC_CONF_OFFSET,
	  offsetof(ngx_http_javascript_loc_conf_t, basedir),
	  NULL },
	// Cache bytecode in the same directory in .xdr files
	{ ngx_string("javascript_cache_xdr"),
	  NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
	  ngx_conf_set_flag_slot,
	  NGX_HTTP_LOC_CONF_OFFSET,
	  offsetof(ngx_http_javascript_loc_conf_t, cache_xdr),
	  NULL },
	// MIME-type to be parsed as server-side JavaScript
	{ ngx_string("javascript_magic"),
	  NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
	  ngx_conf_set_str_slot,
	  NGX_HTTP_LOC_CONF_OFFSET,
	  offsetof(ngx_http_javascript_loc_conf_t, magic),
	  NULL },
	// Maximum size of multipart/form-data
	{ ngx_string("javascript_max_data"),
	  NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
	  ngx_conf_set_size_slot,
	  NGX_HTTP_LOC_CONF_OFFSET,
	  offsetof(ngx_http_javascript_loc_conf_t, max_data),
	  NULL },
	// Maximum size of application/x-www-form-urlencoded
	{ ngx_string("javascript_max_post"),
	  NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
	  ngx_conf_set_size_slot,
	  NGX_HTTP_LOC_CONF_OFFSET,
	  offsetof(ngx_http_javascript_loc_conf_t, max_post),
	  NULL },
	// Maximum memory for JavaScript virtual machine
	{ ngx_string("javascript_memory"),
	  NGX_HTTP_MAIN_CONF|NGX_CONF_TAKE1,
	  ngx_conf_set_size_slot,
	  NGX_HTTP_MAIN_CONF_OFFSET,
	  offsetof(ngx_http_javascript_main_conf_t, memory),
	  NULL },
	// MIME-type for output by default
	{ ngx_string("javascript_output"),
	  NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
	  ngx_conf_set_str_slot,
	  NGX_HTTP_LOC_CONF_OFFSET,
	  offsetof(ngx_http_javascript_loc_conf_t, output),
	  NULL },
	// Maximum memory consumed by one script
	{ ngx_string("javascript_quota"),
	  NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
	  ngx_conf_set_size_slot,
	  NGX_HTTP_LOC_CONF_OFFSET,
	  offsetof(ngx_http_javascript_loc_conf_t, quota),
	  NULL },
	// Execution timeout
	{ ngx_string("javascript_timeout"),
	  NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
	  ngx_conf_set_num_slot,
	  NGX_HTTP_LOC_CONF_OFFSET,
	  offsetof(ngx_http_javascript_loc_conf_t, timeout),
	  NULL },
	ngx_null_command
};

static ngx_http_module_t ngx_http_javascript_module_ctx = {
	NULL,                                  /* preconfiguration */
	NULL,                                  /* postconfiguration */
	ngx_http_javascript_create_main_conf,  /* create main configuration */
	ngx_http_javascript_init_main_conf,    /* init main configuration */
	NULL,                                  /* create server configuration */
	NULL,                                  /* merge server configuration */
	ngx_http_javascript_create_loc_conf,   /* create location configuration */
	ngx_http_javascript_merge_loc_conf     /* merge location configuration */
};

ngx_module_t ngx_http_javascript_module = {
	NGX_MODULE_V1,
	&ngx_http_javascript_module_ctx,       /* module context */
	ngx_http_javascript_commands,          /* module directives */
	NGX_HTTP_MODULE,                       /* module type */
	NULL,                                  /* init master */
	NULL,                                  /* init module */
	NULL,                                  /* init process */
	NULL,                                  /* init thread */
	NULL,                                  /* exit thread */
	NULL,                                  /* exit process */
	NULL,                                  /* exit master */
	NGX_MODULE_V1_PADDING
};

static JSClass ngx_javascript_global_class = {
	"Global",
	JSCLASS_GLOBAL_FLAGS,
	JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub,
	JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, JS_FinalizeStub,
	JSCLASS_NO_OPTIONAL_MEMBERS
};

# ifdef NGX_JAVASCRIPT_HAVE_MYSQL
#include <mysql.h>
static char *ngx_mysql_groups[] = {
	"client",
	"javascript",
	(char *) NULL
};
# endif /* NGX_JAVASCRIPT_HAVE_MYSQL */

void *ngx_http_javascript_create_main_conf(ngx_conf_t *cf) {
    ngx_http_javascript_main_conf_t *conf;
	conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_javascript_main_conf_t));
	if (conf == NULL) {
		return NGX_CONF_ERROR;
	}
	conf->memory = NGX_CONF_UNSET;
	return conf;
}

char *ngx_http_javascript_init_main_conf(ngx_conf_t *cf, void *conf) {
	ngx_http_javascript_main_conf_t *main_conf = conf;
	JS_SetCStringsAreUTF8();
	main_conf->jsr = JS_NewRuntime(main_conf->memory == NGX_CONF_UNSET_SIZE ? 32L * 1024L * 1024L : main_conf->memory);
	if (main_conf->jsr == NULL) {
		ngx_conf_log_error(NGX_LOG_ERR, cf, 0, "JS_NewRuntime() failed");
		return NGX_CONF_ERROR;
	}
# ifdef NGX_JAVASCRIPT_HAVE_MYSQL
	mysql_library_init(0, NULL, ngx_mysql_groups);
# endif /* NGX_JAVASCRIPT_HAVE_MYSQL */
	return NGX_CONF_OK;
}

void *ngx_http_javascript_create_loc_conf(ngx_conf_t *cf) {
	ngx_http_javascript_loc_conf_t *conf;
	conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_javascript_loc_conf_t));
	if (conf == NULL) {
		return NGX_CONF_ERROR;
	}
	conf->quota = NGX_CONF_UNSET_SIZE;
	conf->max_post = NGX_CONF_UNSET_SIZE;
	conf->max_data = NGX_CONF_UNSET_SIZE;
	conf->enable = NGX_CONF_UNSET;
	conf->timeout = NGX_CONF_UNSET;
	conf->cache_xdr = NGX_CONF_UNSET;
	return conf;
}

char *ngx_http_javascript_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) {
	ngx_http_javascript_loc_conf_t *prev = parent;
	ngx_http_javascript_loc_conf_t *conf = child;
	ngx_conf_merge_size_value(conf->quota, prev->quota, 32L * 1024L * 1024L);
	ngx_conf_merge_size_value(conf->max_post, prev->max_post, 1L * 1024L * 1024L);
	ngx_conf_merge_size_value(conf->max_data, prev->max_data, 2L * 1024L * 1024L);
	ngx_conf_merge_value(conf->enable, prev->enable, 1);
	ngx_conf_merge_value(conf->timeout, prev->timeout, 60);
	ngx_conf_merge_value(conf->cache_xdr, prev->cache_xdr, 0);
	ngx_conf_merge_str_value(conf->magic, prev->magic, "application/x-javascript-serverside");
	ngx_conf_merge_str_value(conf->output, prev->output, "text/html");
	ngx_conf_merge_str_value(conf->basedir, prev->basedir, NULL);
	// If basedir is specified in config in the form folder:folder:...
	// Then realdir has the for realpath\0realpah\0...
	if (conf->realdir.len == 0 && conf->realdir.data == NULL && conf->basedir.len > 0 && conf->basedir.data != NULL) {
		size_t i, j, l;
		char basedir[NGX_MAX_PATH];
		char *realdir;
		int parts = 1;
		basedir[0] = '\0';
		strncat(basedir, (char *) conf->basedir.data, conf->basedir.len);
		for (i = 0; i < conf->basedir.len; i ++) if (basedir[i] == ':') {
			basedir[i] = '\0';
			parts ++;
		}
		realdir = ngx_pcalloc(cf->pool, parts * NGX_MAX_PATH);
		for (i = 0, j = 0; i < conf->basedir.len;) {
			l = strlen(basedir + i);
			if (l == 0) {
				i ++;
			} else if (l == 1 && (basedir[i] == '~' || basedir[i] == '.')) {
				realdir[j] = basedir[i]; j ++;
				realdir[j] = '\0'; j ++;
				i += 2;
			} else if (ngx_realpath(basedir + i, realdir + j) != NULL) {
				i += l + 1;
				j += strlen(realdir + j) + 1;
				realdir[j - 1] = '\0';
			} else {
				i += l + 1;
			}
		}
		if (j > 0) {
			conf->realdir.len = j;
			conf->realdir.data = (u_char *) realdir;
		} else {
			ngx_pfree(cf->pool, realdir);
		}
	}
	return NGX_CONF_OK;
}

char *ngx_http_javascript_init_post(ngx_conf_t *cf, void *cmd, void *conf) {
	ngx_http_javascript_main_conf_t *cmcf;
	ngx_http_core_loc_conf_t *clcf;
	cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);
	clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
	clcf->handler = ngx_http_javascript_handler;
	return NGX_CONF_OK;
}

/*
 * Request handler. Minimal memory usage, minimum overhead
 */
ngx_int_t ngx_http_javascript_handler(ngx_http_request_t *r) {
	ngx_int_t rc = NGX_OK;
	ngx_http_javascript_loc_conf_t *conf = ngx_http_get_module_loc_conf(r, ngx_http_javascript_module);
	// Check zero
	if (r->zero_in_uri) {
		return NGX_HTTP_NOT_FOUND;
	}
	// Check enabled
	if (conf->enable != 1) {
		return NGX_DECLINED;
	}
	// Check extension
	if (r->exten.data == NULL) {
		ngx_http_set_exten(r);
	}
	// Check content type
	if (r->headers_out.content_type.data == NULL) {
		ngx_http_set_content_type(r);
	}
	// Check match
	if (r->headers_out.content_type.len != conf->magic.len || ngx_strncmp(r->headers_out.content_type.data, conf->magic.data, conf->magic.len) != 0) {
		return NGX_DECLINED;
	}
	// Check post
	if (r->headers_in.content_length_n <= 0) {
		// Content-Length = 0
		rc = ngx_http_javascript_handle_request(r);
		return rc;
	} else {
		// Content-Length > 0
		ngx_table_elt_t *content_type = r->headers_in.content_type;
		if (content_type->value.len >= sizeof("application/x-www-form-urlencoded") - 1 && ngx_strncmp(content_type->value.data, "application/x-www-form-urlencoded", sizeof("application/x-www-form-urlencoded") - 1) == 0) {
			if (r->headers_in.content_length_n > conf->max_post) {
				return NGX_HTTP_REQUEST_ENTITY_TOO_LARGE;
			}
			r->request_body_file_log_level = 0;
			rc = ngx_http_read_client_request_body(r, ngx_http_javascript_handle_request_data);
			if (rc >= NGX_HTTP_SPECIAL_RESPONSE) return rc;
			return NGX_DONE;
		} else if (content_type->value.len >= sizeof("multipart/form-data") - 1 && ngx_strncmp(content_type->value.data, "multipart/form-data", sizeof("multipart/form-data") - 1) == 0) {
			if (r->headers_in.content_length_n > conf->max_data) {
				return NGX_HTTP_REQUEST_ENTITY_TOO_LARGE;
			}
			r->request_body_file_log_level = 0;
			rc = ngx_http_read_client_request_body(r, ngx_http_javascript_handle_request_data);
			if (rc >= NGX_HTTP_SPECIAL_RESPONSE) return rc;
			return NGX_DONE;
		} else {
			ngx_http_discard_request_body(r);
			rc = ngx_http_javascript_handle_request(r);
			return rc;
		}
	}
}

/*
 * Void version of main request handler
 */
void ngx_http_javascript_handle_request_data(ngx_http_request_t *r) {
	ngx_int_t rc = ngx_http_javascript_handle_request(r);
	ngx_http_finalize_request(r, rc);
}

/*
 * Main request handler
 */
ngx_int_t ngx_http_javascript_handle_request(ngx_http_request_t *r) {
	
	// File
	u_char *p;
	// Root
	size_t root;
	// Path
	ngx_str_t path;
	// Location config
	ngx_http_javascript_loc_conf_t *conf;
	// Main config
	ngx_http_javascript_main_conf_t *main_conf;
	// Private context for JavaScript calls
	ngx_javascript_private_t private;
	// Log
	ngx_log_t *log;
	// Return code
	ngx_int_t rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
	// Result buffer
	ngx_buf_t *b;
	// Output chain
	ngx_chain_t out;
	
	// JavaScript
	jsval result;
	JSBool ok;
	JSContext *jcx;
	JSObject *global;
	JSString *output;
	u_char *output_str;
	jschar *output_str_uc;
	size_t output_len, output_len_uc;
	
	// Objects
	JSObject *global_result;
	JSObject *global_server;
	JSObject *global_request;
	JSObject *global_scripts;
	JSObject *global_request_get;
	JSObject *global_request_post;
	JSObject *global_request_cookie;
	JSObject *global_request_headers;
	JSObject *global_result_headers;
	JSObject *global_file;
	JSObject *global_line;
	
	// Get location configuration
	conf = ngx_http_get_module_loc_conf(r, ngx_http_javascript_module);
	
	// Private context for JavaScript calls
	private.r = r;
	
	// Log
	log = r->connection->log;
	
	// Map URI to path
	p = ngx_http_map_uri_to_path(r, &path, &root, 0);
	if (p == NULL) {
		ngx_log_error(NGX_LOG_ERR, log, 0, "map_uri_to_path() failed");
		return rc;
	}
	private.path = path;
	private.root = root;
	
	// Check file
	// Without this code if requested file does not exist error should be handled by JavaScript engine
	// That is not quite OK, so we check it before starting script
	{
	ngx_uint_t level;
	ngx_open_file_info_t of;
	ngx_http_core_loc_conf_t *clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
	ngx_memzero(&of, sizeof(ngx_open_file_info_t));
	of.read_ahead = clcf->read_ahead;
	of.directio = clcf->directio;
	of.valid = clcf->open_file_cache_valid;
	of.min_uses = clcf->open_file_cache_min_uses;
	of.errors = clcf->open_file_cache_errors;
	of.events = clcf->open_file_cache_events;
	if (ngx_open_cached_file(clcf->open_file_cache, &path, &of, r->pool) != NGX_OK) {
		switch (of.err) {
			case 0:
				return NGX_HTTP_INTERNAL_SERVER_ERROR;
			case NGX_ENOENT:
			case NGX_ENOTDIR:
			case NGX_ENAMETOOLONG:
				return NGX_DECLINED;
			case NGX_EACCES:
				level = NGX_LOG_ERR;
				break;
			default:
				level = NGX_LOG_CRIT;
				break;
        }
		ngx_log_error(level, log, of.err, "%s \"%s\" failed", of.failed, path.data);
		return NGX_DECLINED;
    }
	}
	
	// Runtime
	main_conf = ngx_http_get_module_main_conf(r, ngx_http_javascript_module);
	
	// Create Context
	jcx = JS_NewContext(main_conf->jsr, 8192);
	if (jcx == NULL) {
		// Internal Server Error
		ngx_log_error(NGX_LOG_ERR, log, 0, "JS_NewContext() failed");
		return rc;
	}
	// Set stack quota
	if (conf->quota != NGX_CONF_UNSET_SIZE) {
		JS_SetScriptStackQuota(jcx, conf->quota);
	}
	// Set timeout
	if (conf->timeout > 1) {
		private.timeout = conf->timeout;
		JS_SetOperationCallback(jcx, ngx_javascript_timeout);
	}
	// Begin Request (multithread version of SpiderMonkey/TraceMonkey requires this)
	JS_BeginRequest(jcx);
	// Root all variables (to protect them all from garbage collection)
	JS_EnterLocalRootScope(jcx);
	
	// JavaScript functions need private
	JS_SetContextPrivate(jcx, &private);
	// JavaScript options
	JS_SetOptions(jcx, JSOPTION_VAROBJFIX | JSOPTION_XML);
	// JavaScript errors
	JS_SetErrorReporter(jcx, ngx_javascript_global_error);
	
	// JavaScript global object
	global = JS_NewObject(jcx, &ngx_javascript_global_class, NULL, NULL);
	if (global == NULL) {
		// Internal Server Error
		ngx_log_error(NGX_LOG_ERR, log, 0, "JS_NewObject() failed");
		goto javascript_finalize;
	}
	JS_SetGlobalObject(jcx, global);
	
	// JavaScript standard classes
	if (!JS_InitStandardClasses(jcx, global)) {
		// Internal Server Error
		ngx_log_error(NGX_LOG_ERR, log, 0, "JS_InitStandardClasses() failed");
		goto javascript_finalize;
	}
	
# ifdef NGX_JAVASCRIPT_HAVE_FILE
	ngx_javascript_init_file(jcx, global);
# endif /* NGX_JAVASCRIPT_HAVE_FILE */
	
# ifdef NGX_JAVASCRIPT_HAVE_MYSQL
	ngx_javascript_init_mysql(jcx, global);
# endif /* NGX_JAVASCRIPT_HAVE_MYSQL */
	
# ifdef NGX_JAVASCRIPT_HAVE_SQLITE
	ngx_javascript_init_sqlite(jcx, global);
# endif /* NGX_JAVASCRIPT_HAVE_SQLITE */
	
	// JavaScript global variables and functions
	JS_DefineProperty(jcx, global, "global", OBJECT_TO_JSVAL(global), NULL, NULL, JSPROP_READONLY);
	JS_DefineFunction(jcx, global, "load", ngx_javascript_global_load, 0, JSPROP_ENUMERATE);
	JS_DefineFunction(jcx, global, "include", ngx_javascript_global_load, 0, JSPROP_ENUMERATE);
	JS_DefineFunction(jcx, global, "alert", ngx_javascript_global_alert, 0, JSPROP_ENUMERATE);
	JS_DefineFunction(jcx, global, "print", ngx_javascript_global_print, 0, JSPROP_ENUMERATE);
	JS_DefineFunction(jcx, global, "sleep", ngx_javascript_global_sleep, 0, JSPROP_ENUMERATE);
	JS_DefineFunction(jcx, global, "usleep", ngx_javascript_global_usleep, 0, JSPROP_ENUMERATE);
	
	global_scripts = JS_NewObject(jcx, NULL, NULL, NULL);
	global_request = JS_NewObject(jcx, NULL, NULL, NULL);
	global_result = JS_NewObject(jcx, NULL, NULL, NULL);
	global_server = JS_NewObject(jcx, NULL, NULL, NULL);
	
	global_request_get = JS_NewObject(jcx, NULL, NULL, NULL);
	global_request_post = JS_NewObject(jcx, NULL, NULL, NULL);
	global_request_cookie = JS_NewObject(jcx, NULL, NULL, NULL);
	global_request_headers = JS_NewArrayObject(jcx, 0, NULL);
	
	global_result_headers = JS_NewArrayObject(jcx, 0, NULL);
	
	global_file = JS_NewObject(jcx, NULL, NULL, NULL);
	global_line = JS_NewObject(jcx, NULL, NULL, NULL);
	
	output = JS_NewGrowableString(jcx, NULL, 0);
	
	if (output != NULL)
		JS_DefineProperty(jcx, global, "$output", STRING_TO_JSVAL(output), NULL, NULL, JSPROP_ENUMERATE);
	
	if (global_server != NULL)
		JS_DefineProperty(jcx, global, "$server", OBJECT_TO_JSVAL(global_server), NULL, NULL, JSPROP_ENUMERATE);
	if (global_result != NULL)
		JS_DefineProperty(jcx, global, "$result", OBJECT_TO_JSVAL(global_result), NULL, NULL, JSPROP_ENUMERATE);
	if (global_request != NULL)
		JS_DefineProperty(jcx, global, "$request", OBJECT_TO_JSVAL(global_request), NULL, NULL, JSPROP_ENUMERATE);
	if (global_scripts != NULL)
		JS_DefineProperty(jcx, global, "$scripts", OBJECT_TO_JSVAL(global_scripts), NULL, NULL, JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT);
	
	if (global_file != NULL) {
		JS_DefineFunction(jcx, global_file, "toString", ngx_javascript_file_string, 0, 0);
		JS_DefineProperty(jcx, global, "__FILE__", OBJECT_TO_JSVAL(global_file), NULL, NULL, JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT);
	}
	if (global_line != NULL) {
		JS_DefineFunction(jcx, global_line, "toString", ngx_javascript_line_string, 0, 0);
		JS_DefineProperty(jcx, global, "__LINE__", OBJECT_TO_JSVAL(global_line), NULL, NULL, JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT);
	}
	
	if (global_request != NULL) {
		if (global_request_get != NULL)
			JS_DefineProperty(jcx, global_request, "get", OBJECT_TO_JSVAL(global_request_get), NULL, NULL, JSPROP_ENUMERATE);
		if (global_request_post != NULL)
			JS_DefineProperty(jcx, global_request, "post", OBJECT_TO_JSVAL(global_request_post), NULL, NULL, JSPROP_ENUMERATE);
		if (global_request_cookie != NULL)
			JS_DefineProperty(jcx, global_request, "cookie", OBJECT_TO_JSVAL(global_request_cookie), NULL, NULL, JSPROP_ENUMERATE);
		if (global_request_headers != NULL)
			JS_DefineProperty(jcx, global_request, "headers", OBJECT_TO_JSVAL(global_request_headers), NULL, NULL, JSPROP_ENUMERATE);
	}
	
	if (global_result != NULL) {
		JS_DefineProperty(jcx, global_result, "status", INT_TO_JSVAL(200), NULL, NULL, JSPROP_ENUMERATE);
		if (global_result_headers != NULL)
			JS_DefineProperty(jcx, global_result, "headers", OBJECT_TO_JSVAL(global_result_headers), NULL, NULL, JSPROP_ENUMERATE);
	}
	
	// Server
	if (global_server != NULL) {
		ngx_javascript_read_server(jcx, global_server, r);
	}
	
	// Request
	if (global_request_get != NULL)
		ngx_javascript_read_request(jcx, global_request_get, r, NGX_JAVASCRIPT_PARSE_GET);
	if (global_request_post != NULL)
		ngx_javascript_read_request(jcx, global_request_post, r, NGX_JAVASCRIPT_PARSE_POST);
	if (global_request_cookie != NULL)
		ngx_javascript_read_request(jcx, global_request_cookie, r, NGX_JAVASCRIPT_PARSE_COOKIE);
	
	// Headers
	if (global_request_headers != NULL && global_server != NULL)
		ngx_javascript_read_headers(jcx, global_request_headers, global_server, r);
	
	// Default output is 200, text/html
	if (r->headers_out.status == NGX_OK) r->headers_out.status = NGX_HTTP_OK;
	r->headers_out.content_length_n = 0;
	r->headers_out.content_type.len = conf->output.len;
	r->headers_out.content_type.data = conf->output.data;
	
	log->action = "JavaScript execution";
	ok = ngx_javascript_execute_script(jcx, global, (char *) path.data, &result);
	
	if (ok == JS_FALSE) {
		goto javascript_finalize;
	}
	
	log->action = "JavaScript sending";
	
	// $result object which might have been redefined
	if (JS_LookupProperty(jcx, global, "$result", &result) == JS_TRUE && JSVAL_IS_OBJECT(result) && !JSVAL_IS_NULL(result)) {
		global_result = JSVAL_TO_OBJECT(result);
	}
	// Status
	if (JS_LookupProperty(jcx, global_result, "status", &result) == JS_TRUE) {
		int status;
		if (JS_ValueToInt32(jcx, result, &status) == JS_TRUE && status > 0 && status < 1000) {
			r->headers_out.status = status;
		}
	}
	// Headers
	if (JS_LookupProperty(jcx, global_result, "headers", &result) == JS_TRUE && JSVAL_IS_OBJECT(result) && !JSVAL_IS_NULL(result)) {
		global_result_headers = JSVAL_TO_OBJECT(result);
	}
	// Set headers
	ngx_javascript_write_headers(jcx, global_result_headers, r);
	
	// Output string
	if (JS_LookupProperty(jcx, global, "$output", &result) == JS_TRUE && JSVAL_IS_STRING(result)) {
		output = JSVAL_TO_STRING(result);
	} else {
		output = JSVAL_TO_STRING(JS_GetEmptyStringValue(jcx));
	}
	
	// Output length and bytes
	output_len_uc = JS_GetStringLength(output);
	output_str_uc = JS_GetStringChars(output);
	output_str = (u_char *) JS_GetStringBytes(output);
	JS_EncodeCharacters(jcx, output_str_uc, output_len_uc, NULL, &output_len);
	
	// Only headers
	if (r->method == NGX_HTTP_HEAD) {
		r->headers_out.content_length_n = output_len;
		rc = ngx_http_send_header(r);
		goto javascript_finalize;
	}
	
	// Output buffer
	b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
	if (b == NULL) {
		// Internal Server Error
		ngx_log_error(NGX_LOG_ERR, log, 0, "output failed");
		goto javascript_finalize;
	}
	b->pos = ngx_pnalloc(r->pool, output_len);
	if (b->pos == NULL) {
		// Internal Server Error
		ngx_log_error(NGX_LOG_ERR, log, 0, "output failed");
		goto javascript_finalize;
	}
	b->last = b->pos + output_len;
	if (output_len > 0) b->memory = 1;
	b->last_buf = 1;
	ngx_memcpy(b->pos, output_str, output_len);
	out.buf = b;
	out.next = NULL;
	
	r->headers_out.content_length_n = output_len;
	
	rc = ngx_http_send_header(r);
	
	if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) {
		goto javascript_finalize;
	}
	
	rc = ngx_http_output_filter(r, &out);
	
	javascript_finalize:
	JS_LeaveLocalRootScope(jcx);
	JS_EndRequest(jcx);
	JS_DestroyContextMaybeGC(jcx);
	return rc;
}

/*
 * JavaScript function executed periodically to check script execution timeout
 */
JSBool ngx_javascript_timeout(JSContext *jcx) {
	ngx_javascript_private_t *private = JS_GetContextPrivate(jcx);
	if (private == NULL || private->r == NULL) return JS_FALSE;
	time_t now = time(NULL);
	if (now - private->r->start_sec > private->timeout) return JS_FALSE;
	return JS_TRUE;
}

/*
 * JavaScript report out of memory
 */
JSBool ngx_javascript_outofmemory(JSContext *jcx) {
	JS_ReportOutOfMemory(jcx);
	return JS_FALSE;
}

/*
 * JavaScript error handler
 */
void ngx_javascript_global_error(JSContext *jcx, const char *message, JSErrorReport *report) {
	ngx_javascript_private_t *private = JS_GetContextPrivate(jcx);
	if (private == NULL || private->r == NULL) return;
	ngx_log_error(NGX_LOG_ERR, private->r->connection->log, 0, "%s:%i: %s", report->filename ? report->filename : "-", (unsigned int) report->lineno, message);
}

/*
 * JavaScript log handler
 */
void ngx_javascript_log(JSContext *jcx, const char *message) {
	ngx_javascript_private_t *private = JS_GetContextPrivate(jcx);
	if (private == NULL || private->r == NULL) return;
	ngx_log_error(NGX_LOG_ERR, private->r->connection->log, 0, "%s", message);
}

/*
 * JavaScript alert handler
 */
JSBool ngx_javascript_global_alert(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	ngx_javascript_private_t *private = JS_GetContextPrivate(jcx);
	JSString *str;
	if (private == NULL || private->r == NULL) return ngx_javascript_error(jcx, NGX_JAVASCRIPT_PRIVATE_ERROR);
	if (argc < 1) return JS_TRUE;
	str = JS_ValueToString(jcx, argv[0]);
	if (str == NULL) return JS_TRUE;
	ngx_log_error(NGX_LOG_ALERT, private->r->connection->log, 0, "alert: %s", JS_GetStringBytes(str));
	return JS_TRUE;
}

/*
 * JavaScript print handler
 */
JSBool ngx_javascript_global_print(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	ngx_javascript_private_t *private;
	JSObject *global;
	JSString *old, *str, *output;
	jsval result;
	unsigned int i;
	if (argc < 1) return JS_TRUE;
	private = JS_GetContextPrivate(jcx);
	if (private == NULL || private->r == NULL) return ngx_javascript_error(jcx, NGX_JAVASCRIPT_PRIVATE_ERROR);
	global = JS_GetGlobalObject(jcx);
	if (global == NULL) return ngx_javascript_error(jcx, NGX_JAVASCRIPT_PRIVATE_ERROR);
	if (JS_LookupProperty(jcx, global, "$output", &result) == JS_TRUE && JSVAL_IS_STRING(result)) {
		output = JSVAL_TO_STRING(result);
	} else {
		output = JS_NewGrowableString(jcx, NULL, 0);
		if (output == NULL) return ngx_javascript_outofmemory(jcx);
		result = STRING_TO_JSVAL(output);
		JS_DefineProperty(jcx, global, "$output", result, NULL, NULL, JSPROP_ENUMERATE);
	}
	for (i = 0; i < argc; i ++) {
		old = JS_ValueToString(jcx, argv[i]);
		if (old == NULL) continue;
		str = JS_ConcatStrings(jcx, output, old);
		if (str == NULL) {
			return ngx_javascript_outofmemory(jcx);
		} else if (str != output) {
			output = str;
			result = STRING_TO_JSVAL(output);
			JS_DefineProperty(jcx, global, "$output", result, NULL, NULL, JSPROP_ENUMERATE);
		}
	}
	return JS_TRUE;
}

/*
 * JavaScript load handler
 */
JSBool ngx_javascript_global_load(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	JSBool ok;
	JSString *str;
	char *filename;
	char realname[NGX_MAX_PATH];
	if (argc < 1) return JS_TRUE;
	str = JS_ValueToString(jcx, argv[0]);
	if (str == NULL) return ngx_javascript_error(jcx, "Load failed");
	filename = JS_GetStringBytes(str);
	if (filename[0] == '/') {
		if (ngx_realpath(filename, realname) == NULL) return ngx_javascript_error(jcx, "Load failed");
	} else {
		char fullname[NGX_MAX_PATH];
		if (ngx_javascript_fullpath(jcx, filename, fullname) == JS_FALSE) return JS_FALSE;
		if (ngx_realpath(fullname, realname) == NULL) return ngx_javascript_error(jcx, "Load failed");
	}
	if (ngx_javascript_check_basedir_realpath(jcx, realname) == JS_FALSE) return ngx_javascript_error(jcx, "Outside basedir");
	ok = ngx_javascript_execute_script(jcx, obj, realname, rval);
	if (*rval == JSVAL_VOID) *rval = JSVAL_TRUE;
	return ok;
}

/*
 * JavaScript sleep handler
 */
JSBool ngx_javascript_global_sleep(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	int t;
	if (argc < 1) return JS_TRUE;
	if (JS_ValueToInt32(jcx, argv[0], &t) == JS_TRUE && t > 0) sleep(t);
	return JS_TRUE;
}

/*
 * JavaScript usleep handler
 */
JSBool ngx_javascript_global_usleep(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	int t;
	if (argc < 1) return JS_TRUE;
	if (JS_ValueToInt32(jcx, argv[0], &t) == JS_TRUE && t > 0) usleep(t);
	return JS_TRUE;
}

/*
 * JavaScript $server object
 */
JSBool ngx_javascript_read_server(JSContext *jcx, JSObject *obj, ngx_http_request_t *r) {
	jsval result;
	JSString *str;
	ngx_str_t s;
	ngx_javascript_private_t *private = JS_GetContextPrivate(jcx);
	ngx_http_core_srv_conf_t  *cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);
	if (private != NULL) {
		str = JS_NewStringCopyN(jcx, (char *) private->path.data, private->root);
		if (str != NULL) {
			result = STRING_TO_JSVAL(str);
			JS_DefineProperty(jcx, obj, "documentRoot", result, NULL, NULL, JSPROP_ENUMERATE);
		}
		str = JS_NewStringCopyN(jcx, (char *) private->path.data, private->path.len - 1);
		if (str != NULL) {
			result = STRING_TO_JSVAL(str);
			JS_DefineProperty(jcx, obj, "scriptFileName", result, NULL, NULL, JSPROP_ENUMERATE);
			JS_DefineProperty(jcx, obj, "pathTranslated", result, NULL, NULL, JSPROP_ENUMERATE);
		}
	}
	str = JS_NewStringCopyN(jcx, (char *) r->unparsed_uri.data, r->unparsed_uri.len);
	if (str != NULL) {
		result = STRING_TO_JSVAL(str);
		JS_DefineProperty(jcx, obj, "requestURI", result, NULL, NULL, JSPROP_ENUMERATE);
	}
	str = JS_NewStringCopyN(jcx, (char *) r->args.data, r->args.len);
	if (str != NULL) {
		result = STRING_TO_JSVAL(str);
		JS_DefineProperty(jcx, obj, "queryString", result, NULL, NULL, JSPROP_ENUMERATE);
	}
	str = JS_NewStringCopyN(jcx, (char *) r->method_name.data, r->method_name.len);
	if (str != NULL) {
		result = STRING_TO_JSVAL(str);
		JS_DefineProperty(jcx, obj, "requestMethod", result, NULL, NULL, JSPROP_ENUMERATE);
	}
	str = JS_NewStringCopyN(jcx, (char *) r->connection->addr_text.data, r->connection->addr_text.len);
	if (str != NULL) {
		result = STRING_TO_JSVAL(str);
		JS_DefineProperty(jcx, obj, "remoteAddr", result, NULL, NULL, JSPROP_ENUMERATE);
	}
	{
	ngx_uint_t p;
	u_char port[5];
	struct sockaddr_in *sin;
#if (NGX_HAVE_INET6)
	struct sockaddr_in6 *sin6;
#endif
	s.data = port;
	switch (r->connection->sockaddr->sa_family) {
#if (NGX_HAVE_INET6)
		case AF_INET6:
			sin6 = (struct sockaddr_in6 *) r->connection->sockaddr;
			p = ntohs(sin6->sin6_port);
			break;
#endif
		default: /* AF_INET */
			sin = (struct sockaddr_in *) r->connection->sockaddr;
			p = ntohs(sin->sin_port);
			break;
	}
	if (p > 0 && p < 65536) s.len = ngx_sprintf(s.data, "%ui", p) - s.data;
	str = JS_NewStringCopyN(jcx, (char *) s.data, s.len);
	if (str != NULL) {
		result = STRING_TO_JSVAL(str);
		JS_DefineProperty(jcx, obj, "remotePort", result, NULL, NULL, JSPROP_ENUMERATE);
	}
	if (ngx_connection_local_sockaddr(r->connection, NULL, 0) == NGX_OK) {
		switch (r->connection->local_sockaddr->sa_family) {
#if (NGX_HAVE_INET6)
			case AF_INET6:
				sin6 = (struct sockaddr_in6 *) r->connection->local_sockaddr;
				p = ntohs(sin6->sin6_port);
				break;
#endif
			default: /* AF_INET */
				sin = (struct sockaddr_in *) r->connection->local_sockaddr;
				p = ntohs(sin->sin_port);
				break;
		}
		if (p > 0 && p < 65536) s.len = ngx_sprintf(s.data, "%ui", p) - s.data;
		str = JS_NewStringCopyN(jcx, (char *) s.data, s.len);
		if (str != NULL) {
			result = STRING_TO_JSVAL(str);
			JS_DefineProperty(jcx, obj, "serverPort", result, NULL, NULL, JSPROP_ENUMERATE);
		}
    }
	}
	str = JS_NewStringCopyN(jcx, (char *) r->http_protocol.data, r->http_protocol.len);
	if (str != NULL) {
		result = STRING_TO_JSVAL(str);
		JS_DefineProperty(jcx, obj, "serverProtocol", result, NULL, NULL, JSPROP_ENUMERATE);
	}
	str = JS_NewStringCopyN(jcx, (char *) cscf->server_name.data, cscf->server_name.len);
	if (str != NULL) {
		result = STRING_TO_JSVAL(str);
		JS_DefineProperty(jcx, obj, "serverName", result, NULL, NULL, JSPROP_ENUMERATE);
	}
	{
	u_char addr[NGX_SOCKADDR_STRLEN];
	s.len = NGX_SOCKADDR_STRLEN;
	s.data = addr;
	if (ngx_connection_local_sockaddr(r->connection, &s, 0) == NGX_OK) {
		str = JS_NewStringCopyN(jcx, (char *) s.data, s.len);
		if (str != NULL) {
			result = STRING_TO_JSVAL(str);
			JS_DefineProperty(jcx, obj, "serverAddr", result, NULL, NULL, JSPROP_ENUMERATE);
		}
	}
	}
	str = JS_NewStringCopyN(jcx, NGINX_VER, sizeof(NGINX_VER) - 1);
	if (str != NULL) {
		result = STRING_TO_JSVAL(str);
		JS_DefineProperty(jcx, obj, "serverSoftware", result, NULL, NULL, JSPROP_ENUMERATE);
	}
	if (ngx_http_auth_basic_user(r) == NGX_OK) {
		s.len = r->headers_in.user.len;
		s.data = r->headers_in.user.data;
		str = JS_NewStringCopyN(jcx, (char *) s.data, s.len);
		if (str != NULL) {
			result = STRING_TO_JSVAL(str);
			JS_DefineProperty(jcx, obj, "authUser", result, NULL, NULL, JSPROP_ENUMERATE);
		}
		s.len = r->headers_in.passwd.len;
		s.data = r->headers_in.passwd.data;
		str = JS_NewStringCopyN(jcx, (char *) s.data, s.len);
		if (str != NULL) {
			result = STRING_TO_JSVAL(str);
			JS_DefineProperty(jcx, obj, "authPasswd", result, NULL, NULL, JSPROP_ENUMERATE);
		}
	}
	return JS_TRUE;
}

/*
 * JavaScript $request.headers object
 */
JSBool ngx_javascript_read_headers(JSContext *jcx, JSObject *obj, JSObject *global_server, ngx_http_request_t *r) {
	size_t i, ih, ihh;
	int uc;
	char c;
	ngx_list_part_t *part = &r->headers_in.headers.part;
	ngx_table_elt_t *data = part->elts;
	ngx_table_elt_t *h;
	char hh[1024];
	JSString *str;
	jsval result;
	int length = 0;
	
	for (i = 0 ;; i ++) {
		if (i >= part->nelts) {
			if (part->next == NULL) {
				break;
			}
			part = part->next;
			data = part->elts;
			i = 0;
		}
		h = data + i;
		if (h->key.len + h->value.len + 2 < 1024) {
			hh[0] = '\0';
			strncat(hh, (char *) h->key.data, h->key.len);
			strcat(hh, ": ");
			strncat(hh, (char *) h->value.data, h->value.len);
			str = JS_NewStringCopyN(jcx, hh, h->key.len + h->value.len + 2);
			if (str != NULL) {
				result = STRING_TO_JSVAL(str);
				JS_DefineElement(jcx, obj, length, result, NULL, NULL, 0); length ++;
			}
			hh[0] = '\0';
			strcat(hh, "http");
			for (ih = 0, ihh = 4, uc = 0; ih < h->key.len; ih ++) {
				c = h->key.data[ih];
				if (uc) {
					if (c >= 'a' && c <= 'z') {
						c -= 32;
						uc = 0;
						hh[ihh] = c; ihh ++;
					} else if (c != '-') {
						uc = 0;
						hh[ihh] = c; ihh ++;
					}
				} else {
					if (c >= 'A' && c <= 'Z') {
						c += 32;
						hh[ihh] = c; ihh ++;
					} else if (c != '-') {
						hh[ihh] = c; ihh ++;
					} else {
						uc = 1;
					}
				}
			}
			hh[ihh] = '\0';
			str = JS_NewStringCopyN(jcx, (char *) h->value.data, h->value.len);
			result = STRING_TO_JSVAL(str);
			JS_DefineProperty(jcx, obj, hh + 4, result, NULL, NULL, JSPROP_ENUMERATE);
			if (hh[4] >= 'a' && hh[4] <= 'z') hh[4] -= 32;
			JS_DefineProperty(jcx, global_server, hh, result, NULL, NULL, JSPROP_ENUMERATE);
		}
	}
	JS_SetArrayLength(jcx, obj, length);
	return JS_TRUE;
}

/*
 * Save result headers to nginx headers_out structure
 */
JSBool ngx_javascript_write_headers(JSContext *jcx, JSObject *obj, ngx_http_request_t *r) {
	char *h, *end, *value;
	JSString *str;
	jsval result;
	ngx_table_elt_t *header;
	jsuint i, length = 0;
	size_t sizeofkey;
	
	if (JS_GetArrayLength(jcx, obj, &length) == JS_TRUE) {
		for (i = 0; i < length; i ++) {
			if (JS_LookupElement(jcx, obj, i, &result) == JS_TRUE) {
				str = JS_ValueToString(jcx, result);
				if (str != NULL) {
					h = JS_GetStringBytes(str);
					end = strchr(h, ':');
					if (end == NULL) continue;
					value = end + 1;
					while (*value == ' ' || *value == '\t' || *value == CR || *value == LF) value ++;
					sizeofkey = end - h + 1;
					if (
						(sizeofkey == sizeof("Date") && ngx_strncasecmp((u_char *) "Date", (u_char *) h, sizeof("Date") - 1) == 0)
						||
						(sizeofkey == sizeof("Server") && ngx_strncasecmp((u_char *) "Server", (u_char *) h, sizeof("Server") - 1) == 0)
						||
						(sizeofkey == sizeof("Content-Length") && ngx_strncasecmp((u_char *) "Content-Length", (u_char *) h, sizeof("Content-Length") - 1) == 0)
					) {
					} else if (sizeofkey == sizeof("Content-Type") && ngx_strncasecmp((u_char *) "Content-Type", (u_char *) h, sizeof("Content-Type") - 1) == 0) {
						r->headers_out.content_type.len = r->headers_out.content_type_len = strlen(h) - (value - h);
						r->headers_out.content_type.data = (u_char *) value;
					} else {
						header = ngx_list_push(&r->headers_out.headers);
						if (header == NULL) {
							return JS_FALSE;
						}
						header->hash = 1;
						header->key.data = (u_char *) h;
						header->key.len = end - h;
						header->value.data = (u_char *) value;
						header->value.len = strlen(h) - (value - h);
					}
				}
			}
		}
	}
	return JS_TRUE;
}

/*
 * Read GET, POST or COOKIE request, depending on arg. Register variables
 */
JSBool ngx_javascript_read_request(JSContext *jcx, JSObject *obj, ngx_http_request_t *r, int arg) {
	u_char *res = NULL, *var, *val, *separator = NULL;
	u_char *strtok_buf = NULL;
	int free_buffer = 0;
	if (arg == NGX_JAVASCRIPT_PARSE_GET) {
		res = ngx_pstrdup0(r->pool, &r->args);
		if (res == NULL) {
			return JS_FALSE;
		} else {
			free_buffer = 1;
		}
	}
	if (arg == NGX_JAVASCRIPT_PARSE_COOKIE) {
		ngx_uint_t n;
		ngx_table_elt_t ** e;
		ngx_str_t s;
		n = r->headers_in.cookies.nelts;
		if (n == 0) {
			return JS_FALSE;
		}
		e = r->headers_in.cookies.elts;
		if (n == 1) {
			s = e[0]->value;
			res = ngx_pstrdup0(r->pool, &s);
			if (res == NULL) {
				return JS_FALSE;
			} else {
				free_buffer = 1;
			}
		} else {
			s = e[0]->value;
			res = ngx_pstrdup0(r->pool, &s);
			if (res == NULL) {
				return JS_FALSE;
			} else {
				free_buffer = 1;
			}
		}
	}
	if (arg == NGX_JAVASCRIPT_PARSE_POST) {
		if (r->request_body != NULL) {
			ngx_str_t content_type = r->headers_in.content_type->value;
			if (content_type.len >= sizeof("application/x-www-form-urlencoded") - 1 && ngx_strncmp(content_type.data, "application/x-www-form-urlencoded", sizeof("application/x-www-form-urlencoded") - 1) == 0) {
				return ngx_javascript_read_application_x_www_form_urlencoded(jcx, obj, r);
			} else if (content_type.len >= sizeof("multipart/form-data") - 1 && ngx_strncmp(content_type.data, "multipart/form-data", sizeof("multipart/form-data") - 1) == 0) {
				if (r->request_body_in_file_only) {
					return ngx_javascript_read_multipart_form_data_file(jcx, obj, r);
				} else {
					return ngx_javascript_read_multipart_form_data(jcx, obj, r);
				}
			}
		}
		return JS_TRUE;
	}
	switch (arg) {
		case NGX_JAVASCRIPT_PARSE_GET:
		case NGX_JAVASCRIPT_PARSE_STRING:
			separator = (u_char *) "&;";
			break;
		case NGX_JAVASCRIPT_PARSE_COOKIE:
			separator = (u_char *) ";\0";
			break;
	}
	var = ngx_javascript_string_token(res, separator, &strtok_buf);
	while (var) {
		val = (u_char *) strchr((char *) var, '=');
		if (arg == NGX_JAVASCRIPT_PARSE_COOKIE) {
			while (*var == ' ' || *var == '\t' || *var == '\r' || *var == '\n') {
				var ++;
			}
			if (var == val || *var == '\0') {
				goto next_cookie;
			}
		}
		if (val) {
			u_char *dst;
			size_t val_len;
			size_t dst_len;
			*val ++ = '\0';
			val_len = strlen((char *) val);
			dst = ngx_pcalloc(r->pool, val_len + 1);
			dst_len = (size_t) dst;
			ngx_unescape_uri(&dst, &val, val_len, NGX_UNESCAPE_URI);
			dst_len = (size_t) dst - dst_len;
			*dst = '\0';
			dst -= dst_len;
			val -= val_len;
			ngx_javascript_register_variable(jcx, obj, r, var, strlen((char *) var), dst, dst_len);
			ngx_pfree(r->pool, dst);
		}
		next_cookie:
		var = ngx_javascript_string_token(NULL, separator, &strtok_buf);
	}
	if (free_buffer) {
		ngx_pfree(r->pool, res);
	}
	return JS_TRUE;
}

/*
 * Get request body as a string. This string should be then free'd
 */
ngx_str_t ngx_javascript_request_body(ngx_http_request_t *r) {
	ngx_http_request_body_t *rb = r->request_body;
	ngx_str_t s;
	s.len = 0;
	s.data = NULL;
	if (rb == NULL) return s;
	if (rb->bufs != NULL) {
		ngx_chain_t *chain = rb->bufs;
		ngx_buf_t *buf = chain->buf;
		if (buf == NULL) return s;
		if (chain->next == NULL) {
			s.data = buf->pos;
			s.len = buf->last - buf->pos;
		} else {
			u_char *p;
			s.len += buf->last - buf->pos;
			while (chain->next != NULL) {
				chain = chain->next;
				buf = chain->buf;
				if (buf == NULL) break;
				s.len += buf->last - buf->pos;
			}
			p = ngx_pnalloc(r->pool, s.len);
			if (p == NULL) {
				s.len = 0;
				return s;
			}
			s.data = p;
			chain = rb->bufs;
			buf = chain->buf;
			p = ngx_cpymem(p, buf->pos, buf->last - buf->pos);
			while (chain->next != NULL) {
				chain = chain->next;
				buf = chain->buf;
				if (buf == NULL) break;
				p = ngx_cpymem(p, buf->pos, buf->last - buf->pos);
			}
		}
		return s;
	}
	if (rb->temp_file != NULL) {
		ngx_temp_file_t *temp = rb->temp_file;
		s.len = temp->offset;
		s.data = ngx_pnalloc(r->pool, s.len);
		if (s.data == NULL) {
			s.len = 0;
			return s;
		}
		ngx_read_file(&temp->file, s.data, s.len, 0);
		return s;
	}
	return s;
}

/*
 * Handle application/x-www-form-urlencoded
 */
JSBool ngx_javascript_read_application_x_www_form_urlencoded(JSContext *jcx, JSObject *obj, ngx_http_request_t *r) {
	ngx_str_t body = ngx_javascript_request_body(r);
	if (body.len == 0) return JS_FALSE;
	u_char *var, *val;
	u_char *s = body.data;
	u_char *e = body.data + body.len;
	u_char *p;
	while (s < e && (p = memchr(s, '&', (e - s)))) {
		*p = '\0';
		last_post_value:
		var = s;
		if ((val = memchr(s, '=', (p - s)))) {
			u_char *dst;
			size_t val_len;
			size_t dst_len;
			*val ++ = '\0';
			dst = memchr(val, 0, e - val);
			val_len = dst == NULL ? e - val : dst - val;
			dst = ngx_pcalloc(r->pool, val_len + 1);
			dst_len = (size_t) dst;
			ngx_unescape_uri(&dst, &val, val_len, NGX_UNESCAPE_URI);
			dst_len = (size_t) dst - dst_len;
			*dst = '\0';
			dst -= dst_len;
			val -= val_len;
			ngx_javascript_register_variable(jcx, obj, r, var, strlen((char *) var), dst, dst_len);
			ngx_pfree(r->pool, dst);
		}
		s = p + 1;
	}
	if (s < e) {
		p = e;
		goto last_post_value;
	}
	if (r->request_body->buf != NULL && r->request_body->buf->pos != body.data) ngx_pfree(r->pool, body.data);
	return JS_TRUE;
}

/*
 * Get boundary from Content-Type header of the request
 */
JSBool ngx_javascript_get_boundary(ngx_http_request_t *r, u_char **boundary_data, size_t *boundary_length) {
	ngx_str_t content_type = r->headers_in.content_type->value;
	u_char *boundary, *boundary_end;
	size_t boundary_len;
	boundary = ngx_strlcasestrn(content_type.data, content_type.data + content_type.len, (u_char *) "boundary=", sizeof("boundary=") - 2);
	if (boundary == NULL) return JS_FALSE;
	boundary += sizeof("boundary=") - 1;
	boundary_len = content_type.data + content_type.len - boundary;
	if (boundary_len <= 0) return JS_FALSE;
	if (boundary[0] == '"') {
		boundary ++;
		boundary_len --;
		boundary_end = memchr(boundary, '"', boundary_len);
		if (boundary_end == NULL) return JS_FALSE;
		boundary_len = boundary_end - boundary;
		if (boundary_len <= 0) return JS_FALSE;
	} else {
		boundary_end = memchr(boundary, ',', boundary_len);
		if (boundary_end != NULL) {
			boundary_len = boundary_end - boundary;
			if (boundary_len <= 0) return JS_FALSE;
		}
	}
	if (boundary_len >= NGX_JAVASCRIPT_BUFFER1) return JS_FALSE;
	*boundary_data = boundary;
	*boundary_length = boundary_len;
	return JS_TRUE;
}

/*
 * Read specified data size from a file
 */
size_t ngx_javascript_read_data_file(ngx_javascript_multi_part_t *m, size_t s) {
	ssize_t n;
	if (m->offset >= m->length) return 0;
	if (s == NGX_JAVASCRIPT_BUFFER && m->l > s) {
		memmove(m->b, m->b + m->l - s, s);
		m->o += m->l - s;
		m->l = s;
		n = ngx_read_file(m->file, (u_char *) (m->b + s), s, m->offset);
	} else {
		m->o += m->l;
		m->l = 0;
		n = ngx_read_file(m->file, (u_char *) (m->b), s, m->offset);
	}
	if (n <= 0) return 0;
	m->offset += n;
	m->l += n;
	return n;
}

/*
 * Offset of next line in file
 */
JSBool ngx_javascript_read_line_file(ngx_javascript_multi_part_t *m, size_t s, size_t *ro, u_char **rp) {
	size_t n;
	u_char *p;
	size_t ms = s - m->o;
	p = memchr(m->b + ms, '\n', m->l - ms);
	if (p != NULL) {
		*ro = (p - m->b) + m->o + 1;
		*rp = p;
		return JS_TRUE;
	}
	if (ms > NGX_JAVASCRIPT_BUFFER) {
		ms -= m->l - NGX_JAVASCRIPT_BUFFER;
		n = ngx_javascript_read_data_file(m, NGX_JAVASCRIPT_BUFFER);
		if (n == 0) {
			*ro = m->length;
			*rp = m->b + m->l;
			return JS_FALSE;
		}
		p = memchr(m->b + ms, '\n', m->l - ms);
		if (p != NULL) {
			*ro = p - m->b + m->o + 1;
			*rp = p;
			return JS_TRUE;
		}
	}
	while ((n = ngx_javascript_read_data_file(m, NGX_JAVASCRIPT_BUFFER2)) > 0) {
		p = memchr(m->b, '\n', m->l);
		if (p != NULL) {
			*ro = p - m->b + m->o + 1;
			*rp = p;
			return JS_TRUE;
		}
	}
	*ro = m->length;
	*rp = m->b + m->l;
	return JS_FALSE;
}

/*
 * Read next boundary from a file. Set variables in m related to headers. Set offset and pointer to data
 */
JSBool ngx_javascript_read_boundary_file(ngx_javascript_multi_part_t *m, size_t s, size_t *ro, u_char **rp) {
	size_t p = s; // Previous start of line (offset)
	size_t n = p; // Next start of line (offset)
	size_t l = 0; // Length of line
	m->hl = 0; // Header length is 0
	m->ho = s; // Header offset is start of search position
	u_char *x;
	while (p < m->length) {
		ngx_javascript_read_line_file(m, p, &n, &x);
		l = n - p;
		if (l == 0) {
			// Can't read more
			return JS_FALSE;
		} else if (l == m->rn && n > s) {
			// end-of-line
			if (n - s < NGX_JAVASCRIPT_BUFFER1 && m->hl == 0) {
				// Whole header is now in read buffer
				// No header was read before
				// Copy it to header buffer
				m->hl = n - s;
				memcpy(m->h, m->b + s - m->o, m->hl);
			}
		} else if (l < NGX_JAVASCRIPT_BUFFER1 && l > m->boundary.len + 2) {
			// Whole string is in read buffer
			// Length of string is enough for boundary
			x = m->b + p - m->o;
			if (x[0] == '-' && x[1] == '-' && ngx_strncmp(x + 2, m->boundary.data, m->boundary.len) == 0) {
				*ro = p + 2 + m->boundary.len;
				*rp = x + 2 + m->boundary.len;
				return JS_TRUE;
			}
		}
		p = n;
	}
	return JS_FALSE;
}

/*
 * Parse multipart/form-data from a file
 */
JSBool ngx_javascript_read_multipart_form_data_file(JSContext *jcx, JSObject *obj, ngx_http_request_t *r) {
	ngx_javascript_multi_part_t mm;
	ngx_javascript_multi_part_t *m = &mm;
	u_char bb[NGX_JAVASCRIPT_BUFFER2];
	u_char hh[NGX_JAVASCRIPT_BUFFER];
	ngx_temp_file_t *temp = r->request_body->temp_file;
	size_t p, n;
	u_char *x;
	u_char *var, *val, *name;
	size_t var_len, val_len, name_len;
	
	memset(m, 0, sizeof(ngx_javascript_multi_part_t));
	m->file = &temp->file;
	m->length = temp->offset;
	m->b = bb;
	m->h = hh;
	m->r = r;
	
	if (ngx_javascript_get_boundary(r, &m->boundary.data, &m->boundary.len) == JS_FALSE) return JS_FALSE;
	
	if (ngx_javascript_read_data_file(m, NGX_JAVASCRIPT_BUFFER2) <= 0) return JS_FALSE;
	
	// read first boundary
	if (ngx_javascript_read_boundary_file(m, 0, &p, &x) == JS_FALSE) {
		// first boundary not found
		return JS_FALSE;
	}
	
	// determine size of end-of-line: 1 (\n) or 2 (\r\n)
	if (x[0] == '\r') {
		m->rn ++;
		x ++;
	}
	if (x[0] == '\n') {
		m->rn ++;
		x ++;
	}
	p += m->rn;
	if (m->rn == 0) return JS_FALSE;
	
	// p points to the start of chunk - right after boundary\r\n
	while (p < m->length) {
		// read chunks separated by boundaries
		var = val = name = NULL;
		var_len = val_len = name_len = 0;
		// read next boundary
		if (ngx_javascript_read_boundary_file(m, p, &n, &x) == JS_FALSE) {
			// next boundary not found
			return JS_FALSE;
		}
		if (m->hl != 0) {
			ngx_javascript_multipart_headers(m, &var, &var_len, &name, &name_len);
			if (var != NULL) {
				if (name == NULL) {
					// normal variable
					val_len = n - m->boundary.len - 2 - m->rn - m->ho - m->hl;
					if (m->ho + m->hl >= m->o && m->ho + m->hl + val_len <= m->o + m->l) {
						val = (u_char *) (m->b + m->ho - m->o + m->hl);
						ngx_javascript_register_variable(jcx, obj, r, var, var_len, val, val_len);
					} else {
						val = ngx_pcalloc(r->pool, val_len);
						if (val == NULL) {
							return JS_FALSE;
						}
						if (ngx_read_file(m->file, val, val_len, m->ho + m->hl) > 0) {
							ngx_javascript_register_variable(jcx, obj, r, var, var_len, val, val_len);
						}
						ngx_pfree(r->pool, val);
					}
				} else {
					// file upload
					ngx_javascript_register_variable(jcx, obj, r, var, var_len, name, name_len);
				}
			}
		}
		p = n;
		if (m->length - p < 2) break;
		if (x[0] == '-' && x[1] == '-') break;
		p += m->rn;
	}
	return JS_TRUE;
}

/*
 * Parse headers of one multi-part data and set variable name and file name
 */
JSBool ngx_javascript_multipart_headers(ngx_javascript_multi_part_t *m, u_char **rvar, size_t *rvar_len, u_char **rname, size_t *rname_len) {
	u_char *var, *name;
	size_t var_len, name_len;
	u_char *h = (u_char *) (m->h); // start of headers
	u_char *n = (u_char *) (m->h + m->hl); // end of headers
	var = name = NULL; // variable name, variable value, file name
	var_len = name_len = 0; // lengths
	
	while (h < n) {
		// read header line
		u_char *x = ngx_javascript_read_line(h, n);
		if ((int) (x - h) <= (int) (m->rn)) {
			// too short header
			break;
		}
		// check header
		if ((size_t) (x - h) >= sizeof("Content-Disposition:") && ngx_strncasecmp(h, (u_char *) "Content-Disposition:", sizeof("Content-Disposition:") - 1) == 0) {
			// content-disposition header
			h += sizeof("Content-Disposition:") - 1;
			// find ; in this header
			h = memchr(h, ';', x - h);
			if (h == NULL) {
				// ; not found, ignore this header and go to next
				h = x;
				continue;
			}
			h ++;
			while (h < x) {
				if (h[0] == ' ' || h[0] == '\t' || h[0] == '\r' || h[0] == '\n') h ++;
				else break;
			}
			// h now points to the start of description after ;\s*
			while (h < x) {
				if ((size_t) (x - h) >= sizeof("name=") && ngx_strncasecmp(h, (u_char *) "name=", sizeof("name") - 1) == 0) {
					h += sizeof("name=") - 1;
					if (h[0] == '"') {
						h ++;
						var = h;
						h = memchr(h, '"', x - h);
						if (h == NULL) {
							var = NULL;
							break;
						} else {
							var_len = h - var;
							var[var_len] = '\0';
							h ++;
							h = memchr(h, ';', x - h);
							if (h == NULL) break;
							h ++;
						}
					} else {
						var = h;
						h = memchr(h, ';', x - h);
						if (h == NULL) {
							var_len = x - var;
							var[var_len] = '\0';
							break;
						} else {
							var_len = h - var;
							var[var_len] = '\0';
							h ++;
						}
					}
				} else if ((size_t) (x - h) >= sizeof("filename=") && ngx_strncasecmp(h, (u_char *) "filename=", sizeof("filename") - 1) == 0) {
					h += sizeof("filename=") - 1;
					if (h[0] == '"') {
						h ++;
						name = h;
						h = memchr(h, '"', x - h);
						if (h == NULL) {
							name = NULL;
							break;
						} else {
							name_len = h - name;
							name[name_len] = '\0';
							h ++;
							h = memchr(h, ';', x - h);
							if (h == NULL) break;
							h ++;
						}
					} else {
						name = h;
						h = memchr(h, ';', x - h);
						if (h == NULL) {
							name_len = x - name;
							name[name_len] = '\0';
							break;
						} else {
							name_len = h - name;
							name[name_len] = '\0';
							h ++;
						}
					}
				}
				while (h < x) {
					if (h[0] == ' ' || h[0] == '\t' || h[0] == '\r' || h[0] == '\n') h ++;
					else break;
				}
			}
		}
		// jump to next header unconditionally
		h = x;
	}
	*rvar = var;
	*rname = name;
	*rvar_len = var_len;
	*rname_len = name_len;
	return JS_TRUE;
}

/*
 * Read one line from data in memory
 */
u_char *ngx_javascript_read_line(u_char *s, u_char *e) {
	u_char *p = memchr(s, '\n', e - s);
	if (p != NULL) return p + 1;
	return e;
}

/*
 * Read boundary from memory
 */
JSBool ngx_javascript_read_boundary(ngx_javascript_multi_part_t *m, u_char *s, u_char **rp) {
	u_char *e = m->b + m->length;
	u_char *p = s;
	u_char *n = p;
	size_t l;
	m->h = s;
	m->hl = 0;
	while (p < e) {
		n = ngx_javascript_read_line(p, e);
		l = n - p;
		if (l == 0) {
			return JS_FALSE;
		} else if (l == m->rn) {
			m->hl = n - s;
		} else if ((size_t) (n - p) > (size_t) (m->boundary.len + 2)) {
			if (p[0] == '-' && p[1] == '-' && ngx_strncmp(p + 2, m->boundary.data, m->boundary.len) == 0) {
				*rp = p + 2 + m->boundary.len;
				return JS_TRUE;
			}
		}
		p = n;
	}
	return JS_FALSE;
}

/*
 * Parse multipart/form-data from a memory
 */
JSBool ngx_javascript_read_multipart_form_data(JSContext *jcx, JSObject *obj, ngx_http_request_t *r) {
	ngx_str_t body;
	ngx_javascript_multi_part_t mm;
	ngx_javascript_multi_part_t *m = &mm;
	u_char *var, *val, *name;
	size_t var_len, val_len, name_len;
	u_char *s, *e, *p, *n;
	memset(m, 0, sizeof(ngx_javascript_multi_part_t));
	
	if (ngx_javascript_get_boundary(r, &m->boundary.data, &m->boundary.len) == JS_FALSE) return JS_FALSE;
	
	body = ngx_javascript_request_body(r);
	if (body.len == 0) return JS_FALSE;
	
	s = body.data;
	e = body.data + body.len;
	
	m->l = m->length = e - s;
	m->b = s;
	m->r = r;
	
	// read first boundary
	if (ngx_javascript_read_boundary(m, s, &p) == JS_FALSE) goto ngx_javascript_read_multipart_form_data_finish;
	
	// determine size of end-of-line: 1 (\n) or 2 (\r\n)
	if (p[0] == '\r') {
		m->rn ++;
		p ++;
	}
	if (p[0] == '\n') {
		m->rn ++;
		p ++;
	}
	if (m->rn == 0) goto ngx_javascript_read_multipart_form_data_finish;
	
	// p points to the start of chunk - right after boundary\r\n
	while (p < e) {
		// read chunks separated by boundaries
		var = val = name = NULL;
		var_len = val_len = name_len = 0;
		// read next boundary
		if (ngx_javascript_read_boundary(m, p, &n) == JS_FALSE) goto ngx_javascript_read_multipart_form_data_finish;
		if (m->hl > 0) {
			ngx_javascript_multipart_headers(m, &var, &var_len, &name, &name_len);
			if (var != NULL) {
				val_len = n - m->h - m->hl - m->boundary.len - 2 - m->rn;
				val = m->h + m->hl;
				if (name == NULL) {
					ngx_javascript_register_variable(jcx, obj, r, var, var_len, val, val_len);
				} else {
					ngx_javascript_register_variable(jcx, obj, r, var, var_len, name, name_len);
				}
			}
		}
		p = n;
		if (e - p < 2) break;
		if (p[0] == '-' && p[1] == '-') break;
		p += m->rn;
	}
	ngx_javascript_read_multipart_form_data_finish:
	if (r->request_body->buf != NULL && r->request_body->buf->pos != body.data) ngx_pfree(r->pool, body.data);
	return JS_TRUE;
}

/*
 * Register javascript variable. Check if variable name contains '\0', '[' or ']', if it has non-zero length and is a correct reference to a property
 */
JSBool ngx_javascript_register_variable(JSContext *jcx, JSObject *obj, ngx_http_request_t *r, u_char *var, size_t var_len, u_char *val, size_t val_len) {
	u_char *dst;
	size_t dst_len;
	size_t i;
	u_char c;
	int ok;
	jsval result;
	dst = ngx_pcalloc(r->pool, var_len + 1);
	dst_len = (size_t) dst;
	ngx_unescape_uri(&dst, &var, var_len, NGX_UNESCAPE_URI);
	dst_len = (size_t) dst - dst_len;
	*dst = '\0';
	dst -= dst_len;
	var -= var_len;
	ok = 1;
	if (!dst_len) {
		ok = 0;
	} else if (dst[0] == '[' || dst[dst_len - 1] == '[') {
		ok = 0;
	} else {
		int open_first = 0;
		int open_count = 0;
		int close_count = 0;
		for (i = 0; i < dst_len; i ++) {
			c = dst[i];
			if (c == '\0') {
				ok = 0;
				break;
			} else if (c == '[') {
				open_count ++;
				if (!open_first) {
					if (close_count) {
						ok = 0;
						break;
					}
					open_first = i;
				}
			} else if (c == ']') {
				close_count ++;
				if (i != dst_len - 1 && dst[i + 1] != '[') {
					ok = 0;
					break;
				}
			}
		}
		if (ok) {
			if (open_count == close_count) {
				if (open_first) {
					ngx_javascript_register_variable_r(jcx, obj, r, dst, dst_len, val, val_len);
					ok = 0;
				}
			} else {
				ok = 0;
			}
		}
	}
	if (ok) {
		result = STRING_TO_JSVAL(JS_NewStringCopyN(jcx, (char *) val, val_len));
		JS_DefineProperty(jcx, obj, (char *) dst, result, NULL, NULL, JSPROP_ENUMERATE);
	}
	ngx_pfree(r->pool, dst);
	return JS_TRUE;
}

/*
 * Register javascript variable in array. That is, variable name contains '[' and ']' and is a correct reference to array element
 */
JSBool ngx_javascript_register_variable_r(JSContext *jcx, JSObject *obj, ngx_http_request_t *r, u_char *var, size_t var_len, u_char *val, size_t val_len) {
	size_t i;
	u_char *p = var;
	JSObject *o, *old;
	jsval result;
	o = old = NULL;
	for (i = 0; i < var_len; i ++) {
		if (var[i] == '[') {
			var[i] = '\0';
			if (JS_GetProperty(jcx, obj, (char *) p, &result) == JS_TRUE && (JSVAL_IS_OBJECT(result))) {
				o = old = JSVAL_TO_OBJECT(result);
			} else {
				o = old = JS_NewArrayObject(jcx, 0, NULL);
				result = OBJECT_TO_JSVAL(o);
				JS_DefineProperty(jcx, obj, (char *) p, result, NULL, NULL, JSPROP_ENUMERATE);
			}
			break;
		}
	}
	if (!o) return JS_TRUE;
	p = var + i + 1;
	for (i = i + 1; i < var_len; i ++) {
		if (var[i] == ']') {
			var[i] = '\0';
			if (p[0] == '\0') {
				jsuint l;
				if (JS_HasArrayLength(jcx, old, &l) == JS_FALSE) {
					l = 0;
				}
				JS_SetArrayLength(jcx, old, l + 1);
				if (i == var_len - 1) {
					result = STRING_TO_JSVAL(JS_NewStringCopyN(jcx, (char *) val, val_len));
					JS_DefineElement(jcx, old, l, result, NULL, NULL, JSPROP_ENUMERATE);
					return JS_TRUE;
				} else {
					o = JS_NewArrayObject(jcx, 0, NULL);
					result = OBJECT_TO_JSVAL(o);
					JS_DefineElement(jcx, old, l, result, NULL, NULL, JSPROP_ENUMERATE);
					p = var + i + 2;
					old = o;
				}
			} else {
				if (i == var_len - 1) {
					result = STRING_TO_JSVAL(JS_NewStringCopyN(jcx, (char *) val, val_len));
					JS_DefineProperty(jcx, old, (char *) p, result, NULL, NULL, JSPROP_ENUMERATE);
					return JS_TRUE;
				} else {
					if (JS_GetProperty(jcx, old, (char *) p, &result) == JS_TRUE && (JSVAL_IS_OBJECT(result))) {
						o = JSVAL_TO_OBJECT(result);
					} else {
						o = JS_NewArrayObject(jcx, 0, NULL);
						result = OBJECT_TO_JSVAL(o);
						JS_DefineProperty(jcx, old, (char *) p, result, NULL, NULL, JSPROP_ENUMERATE);
					}
					p = var + i + 2;
					old = o;
				}
			}
		}
	}
	return JS_TRUE;
}

/*
 * Duplicate string like ngx_pstrdup(), but result is NULL-terminated
 */
u_char *ngx_pstrdup0(ngx_pool_t *pool, ngx_str_t *src) {
	u_char *dst;
	dst = ngx_pnalloc(pool, src->len + 1);
	if (dst == NULL) {
		return NULL;
	}
	ngx_memcpy(dst, src->data, src->len);
	dst[src->len] = '\0';
	return dst;
}

/*
 * Compile and execute script, compiled script may be saved as xdr
 */
JSBool ngx_javascript_execute_script(JSContext *jcx, JSObject *obj, const char *fileName, jsval *rval) {
	ngx_javascript_private_t *private;
	ngx_http_javascript_loc_conf_t *conf;
	ngx_http_request_t *r;
	jsval result;
	JSObject *global = NULL;
	JSObject *global_scripts = NULL;
	JSObject *script_object = NULL;
	JSScript *script = NULL;
	JSBool ok;
	
	private = JS_GetContextPrivate(jcx);
	if (private == NULL) return ngx_javascript_error(jcx, NGX_JAVASCRIPT_PRIVATE_ERROR);
	r = private->r;
	if (r == NULL) return ngx_javascript_error(jcx, NGX_JAVASCRIPT_PRIVATE_ERROR);
	conf = ngx_http_get_module_loc_conf(r, ngx_http_javascript_module);
	if (conf == NULL) return ngx_javascript_error(jcx, NGX_JAVASCRIPT_PRIVATE_ERROR);
	global = JS_GetGlobalObject(jcx);
	if (global == NULL) return ngx_javascript_error(jcx, NGX_JAVASCRIPT_PRIVATE_ERROR);
	if (JS_GetProperty(jcx, global, "$scripts", &result) == JS_TRUE && JSVAL_IS_OBJECT(result) && !JSVAL_IS_NULL(result)) {
		global_scripts = JSVAL_TO_OBJECT(result);
	} else {
		return ngx_javascript_error(jcx, NGX_JAVASCRIPT_PRIVATE_ERROR);
	}
	
	if (JS_GetProperty(jcx, global_scripts, fileName, &result) == JS_TRUE && JSVAL_IS_OBJECT(result)) {
		// When script is being executed this property is set to NULL to forbid recursive script loading
		if (JSVAL_IS_NULL(result)) {
			return ngx_javascript_error(jcx, "Recursion");
		} else {
			script_object = JSVAL_TO_OBJECT(result);
			script = JS_GetPrivate(jcx, script_object);
			if (script != NULL) {
				JS_DefineProperty(jcx, global_scripts, fileName, JSVAL_NULL, NULL, NULL, JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT);
				ok = JS_ExecuteScript(jcx, obj, script, rval);
				JS_DefineProperty(jcx, global_scripts, fileName, result, NULL, NULL, JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT);
				return ok;
			}
		}
	}
	
	if (conf->cache_xdr != 1) {
		size_t sizeb = -1;
		size_t sizec = -1;
		char *fileb = NULL;
		jschar *filec = NULL;
		FILE *F = fopen(fileName, "r");
		if (F == NULL) goto javascript_cache_xdr_0_error;
		if (fseek(F, 0, SEEK_END) != 0) goto javascript_cache_xdr_0_error;
		sizeb = ftell(F);
		if (fseek(F, 0, SEEK_SET) != 0) goto javascript_cache_xdr_0_error;
		fileb = ngx_pnalloc(r->pool, sizeb);
		if (fileb == NULL) goto javascript_cache_xdr_0_error;
		if (fread(fileb, 1, sizeb, F) != sizeb) goto javascript_cache_xdr_0_error;
		fclose(F); F = NULL;
		if (JS_DecodeBytes(jcx, fileb, sizeb, filec, &sizec) == JS_FALSE) goto javascript_cache_xdr_0_error;
		filec = ngx_pnalloc(r->pool, sizec * sizeof(jschar));
		if (filec == NULL) goto javascript_cache_xdr_0_error;
		if (JS_DecodeBytes(jcx, fileb, sizeb, filec, &sizec) == JS_FALSE) goto javascript_cache_xdr_0_error;
		ngx_pfree(r->pool, fileb); fileb = NULL;
		script = JS_CompileUCScript(jcx, obj, filec, sizec, fileName, 1);
		if (script == NULL) goto javascript_cache_xdr_0_error;
		script_object = JS_NewScriptObject(jcx, script);
		JS_DefineProperty(jcx, global_scripts, fileName, JSVAL_NULL, NULL, NULL, JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT);
		ok = JS_ExecuteScript(jcx, obj, script, rval);
		if (script_object != NULL)
			JS_DefineProperty(jcx, global_scripts, fileName, OBJECT_TO_JSVAL(script_object), NULL, NULL, JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT);
		return ok;
		javascript_cache_xdr_0_error:
		if (F != NULL) fclose(F);
		if (fileb != NULL) ngx_pfree(r->pool, fileb);
		if (filec != NULL) ngx_pfree(r->pool, filec);
		return ngx_javascript_error(jcx, "Load failed");
	} else {
		
		void *data = NULL;
		char compiledFileName[NGX_MAX_PATH];
		struct stat fileStat, compiledFileStat;
		struct flock flptr;
		int fd;
		
		if (stat(fileName, &fileStat) == -1) {
			return ngx_javascript_error(jcx, "Load failed");
		}
		
		strcpy(compiledFileName, fileName);
		strcat(compiledFileName, ".xdr");
		flptr.l_start = 0;
		flptr.l_whence = SEEK_SET;
		flptr.l_len = 0;
		
		// check xdr file exists, is up-to-date, is not empty
		if (stat(compiledFileName, &compiledFileStat) != -1 && compiledFileStat.st_mtime > fileStat.st_mtime && compiledFileStat.st_size > 0) {
			// open
			fd = open(compiledFileName, O_RDONLY);
			if (fd == -1) goto javascript_compile;
			// lock
			flptr.l_type = F_RDLCK;
			if (fcntl(fd, F_SETLK, &flptr) != -1) {
				// allocate memory, read
				data = ngx_pnalloc(r->pool, compiledFileStat.st_size);
				if (read(fd, data, compiledFileStat.st_size) != compiledFileStat.st_size) {
					// free, unlock, close
					ngx_pfree(r->pool, data);
					flptr.l_type = F_UNLCK;
					fcntl(fd, F_SETLK, &flptr);
					close(fd);
					goto javascript_compile;
				}
				// unlock, close
				flptr.l_type = F_UNLCK;
				fcntl(fd, F_SETLK, &flptr);
				close(fd);
				// create xdr
				JSXDRState *xdr = JS_XDRNewMem(jcx, JSXDR_DECODE);
				if (xdr == NULL) {
					// create xdr failed, free
					ngx_pfree(r->pool, data);
					goto javascript_compile;
				}
				// set memory
				JS_XDRMemSetData(xdr, data, compiledFileStat.st_size);
				// get script
				ok = JS_XDRScript(xdr, &script);
				// set memory
				JS_XDRMemSetData(xdr, NULL, 0);
				// destroy xdr
				JS_XDRDestroy(xdr);
				// free
				ngx_pfree(r->pool, data); data = NULL;
				if (ok) {
					// execute, free
					goto javascript_execute;
				}
			} else {
				// close
				close(fd);
			}
		}
		// compile
		javascript_compile:
		{
		size_t sizeb = fileStat.st_size;
		size_t sizec = -1;
		char *fileb = NULL;
		jschar *filec = NULL;
		FILE *F = fopen(fileName, "r");
		if (F == NULL) goto javascript_cache_xdr_1_error;
		fileb = ngx_pnalloc(r->pool, sizeb);
		if (fileb == NULL) goto javascript_cache_xdr_1_error;
		if (fread(fileb, 1, sizeb, F) != sizeb) goto javascript_cache_xdr_1_error;
		fclose(F); F = NULL;
		if (JS_DecodeBytes(jcx, fileb, sizeb, filec, &sizec) == JS_FALSE) goto javascript_cache_xdr_1_error;
		filec = ngx_pnalloc(r->pool, sizec * sizeof(jschar));
		if (filec == NULL) goto javascript_cache_xdr_1_error;
		if (JS_DecodeBytes(jcx, fileb, sizeb, filec, &sizec) == JS_FALSE) goto javascript_cache_xdr_1_error;
		ngx_pfree(r->pool, fileb); fileb = NULL;
		script = JS_CompileUCScript(jcx, obj, filec, sizec, fileName, 1);
		ngx_pfree(r->pool, filec); filec = NULL;
		javascript_cache_xdr_1_error:
		if (F != NULL) fclose(F);
		if (fileb != NULL) ngx_pfree(r->pool, fileb);
		if (filec != NULL) ngx_pfree(r->pool, filec);
		if (script == NULL) return JS_FALSE;
		}
		// open
		fd = open(compiledFileName, O_WRONLY | O_CREAT | O_TRUNC, fileStat.st_mode);
		if (fd != -1) {
			// lock
			flptr.l_type = F_WRLCK;
			if (fcntl(fd, F_SETLK, &flptr) != -1) {
				// create xdr
				JSXDRState *xdr = JS_XDRNewMem(jcx, JSXDR_ENCODE);
				if (xdr != NULL) {
					// set script
					if (JS_XDRScript(xdr, &script) == JS_TRUE) {
						uint32 length;
						// get data
						data = JS_XDRMemGetData(xdr, &length);
						if (data != NULL) {
							// write
							write(fd, data, length);
							data = NULL;
						}
					}
					// set memory
					JS_XDRMemSetData(xdr, NULL, 0);
					// destroy xdr
					JS_XDRDestroy(xdr);
				}
				// unlock
				flptr.l_type = F_UNLCK;
				fcntl(fd, F_SETLK, &flptr);
			}
			// close
			close(fd);
		}
		// is *not* null
		if (script == NULL) return ngx_javascript_error(jcx, "Load failed");
		javascript_execute:
		script_object = JS_NewScriptObject(jcx, script);
		JS_DefineProperty(jcx, global_scripts, fileName, JSVAL_NULL, NULL, NULL, JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT);
		ok = JS_ExecuteScript(jcx, obj, script, rval);
		if (script_object != NULL)
			JS_DefineProperty(jcx, global_scripts, fileName, OBJECT_TO_JSVAL(script_object), NULL, NULL, JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT);
		return ok;
	}
}

JSBool ngx_javascript_fullpath(JSContext *jcx, char *filename, char *fullname) {
	ngx_javascript_private_t *private = JS_GetContextPrivate(jcx);
	if (!private) return ngx_javascript_error(jcx, NGX_JAVASCRIPT_PRIVATE_ERROR);
	fullname[0] = '\0';
	strncat(fullname, (char *) private->path.data, private->root);
	strcat(fullname, "/");
	strcat(fullname, filename);
	return JS_TRUE;
}

/*
 * Check filename is in basedir path
 * JS_TRUE if yes, JS_FALSE if no
 * mode is either 0 (file should be accessed read-only) or O_CREATE (create if not exists)
 */
JSBool ngx_javascript_check_basedir(JSContext *jcx, char *filename, int mode) {
	ngx_javascript_private_t *private;
	ngx_http_javascript_loc_conf_t *conf;
	ngx_http_request_t *r;
	private = JS_GetContextPrivate(jcx);
	if (private == NULL) return ngx_javascript_error(jcx, NGX_JAVASCRIPT_PRIVATE_ERROR);
	r = private->r;
	if (r == NULL) return ngx_javascript_error(jcx, NGX_JAVASCRIPT_PRIVATE_ERROR);
	conf = ngx_http_get_module_loc_conf(r, ngx_http_javascript_module);
	if (conf == NULL) return ngx_javascript_error(jcx, NGX_JAVASCRIPT_PRIVATE_ERROR);
	if (conf->realdir.len == 0 || conf->realdir.data == NULL) {
		return JS_TRUE;
	} else {
		char c = '/';
		struct stat st;
		char *slash = NULL;
		if (lstat(filename, &st) == -1) {
			if (mode & O_CREAT) {
				slash = filename + strlen(filename) - 1;
				if (slash - filename <= 0) return JS_FALSE;
				while (*slash == '/' && slash > filename) slash --;
				while (*slash != '/' && slash > filename) slash --;
				while (*slash == '/' && slash > filename) slash --;
				slash ++;
				c = *slash;
				*slash = '\0';
				if (lstat(filename, &st) == -1) {
					*slash = c;
					return JS_FALSE;
				}
			} else {
				return JS_FALSE;
			}
		}
		{
			char realname[NGX_MAX_PATH];
			char *realdir;
			size_t i, l, length, reallen;
			if (ngx_realpath(filename, realname) == NULL) {
				if (slash != NULL) *slash = c;
				return JS_FALSE;
			}
			reallen = strlen(realname);
			realdir = (char *) conf->realdir.data;
			length = conf->realdir.len;
			for (i = 0; i < length; i ++) {
				l = strlen(realdir + i);
				if (l == 0) {
					continue;
				} else if (l == 1 && realdir[i] == '~') {
					if (reallen >= private->root && strncmp(realname, (char *) private->path.data, private->root) == 0 && (realname[private->root] == '/' || realname[private->root] == '\0')) {
						if (slash != NULL) *slash = c;
						return JS_TRUE;
					}
				} else if (reallen >= l) {
					if (strncmp(realname, realdir + i, l) == 0 && (realname[l - 1] == '/' || realname[l] == '/' || realname[l] == '\0')) {
						if (slash != NULL) *slash = c;
						return JS_TRUE;
					}
				}
				i += l;
			}
			if (slash != NULL) *slash = c;
		}
	}
	return JS_FALSE;
}

/*
 * Check filename is in basedir path. filename is real path
 * JS_TRUE if yes, JS_FALSE if no
 * The same as above but doesn't do realpath() and uses less memory
 * Is used primarily in script processing
 */
JSBool ngx_javascript_check_basedir_realpath(JSContext *jcx, const char *filename) {
	ngx_javascript_private_t *private;
	ngx_http_javascript_loc_conf_t *conf;
	ngx_http_request_t *r;
	private = JS_GetContextPrivate(jcx);
	if (private == NULL) return ngx_javascript_error(jcx, NGX_JAVASCRIPT_PRIVATE_ERROR);
	r = private->r;
	if (r == NULL) return ngx_javascript_error(jcx, NGX_JAVASCRIPT_PRIVATE_ERROR);
	conf = ngx_http_get_module_loc_conf(r, ngx_http_javascript_module);
	if (conf == NULL) return ngx_javascript_error(jcx, NGX_JAVASCRIPT_PRIVATE_ERROR);
	if (conf->realdir.len == 0 || conf->realdir.data == NULL) {
		return JS_TRUE;
	} else {
		size_t i, l;
		size_t reallen = strlen(filename);
		char *realdir = (char *) conf->realdir.data;
		size_t length = conf->realdir.len;
		for (i = 0; i < length; i ++) {
			l = strlen(realdir + i);
			if (l == 0) {
				continue;
			} else if (l == 1 && realdir[i] == '~') {
				if (reallen >= private->root && strncmp(filename, (char *) private->path.data, private->root) == 0 && (filename[private->root] == '/' || filename[private->root] == '\0')) {
					return JS_TRUE;
				}
			} else if (reallen >= l) {
				if (strncmp(filename, realdir + i, l) == 0 && (filename[l - 1] == '/' || filename[l] == '/' || filename[l] == '\0')) {
					return JS_TRUE;
				}
			}
			i += l;
		}
	}
	return JS_FALSE;
}

JSBool ngx_javascript_error(JSContext *jcx, const char *message) {
	JS_ReportError(jcx, "%s", message);
	return JS_FALSE;
}

JSBool ngx_javascript_file_line(JSContext *jcx, const char **file, int *line) {
	JSStackFrame *fp = NULL;
	JSScript *script;
	const char *rfile;
	int rline;
	do {
		JS_FrameIterator(jcx, &fp);
	} while (fp && !JS_GetFramePC(jcx, fp));
	if (fp == NULL) return JS_FALSE;
	script = JS_GetFrameScript(jcx, fp);
	if (script == NULL) return JS_FALSE;
	rfile = JS_GetScriptFilename(jcx, script);
	if (rfile == NULL || *rfile == '\0') rfile = "-";
	rline = JS_PCToLineNumber(jcx, script, JS_GetFramePC(jcx, fp));
	*file = rfile;
	*line = rline;
	return JS_TRUE;
}

JSBool ngx_javascript_file_string(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	JSString *str;
	const char *file;
	int line;
	if (ngx_javascript_file_line(jcx, &file, &line) == JS_TRUE) {
		str = JS_NewStringCopyN(jcx, file, strlen(file));
		if (str != NULL) {
			*rval = STRING_TO_JSVAL(str);
		}
	}
	return JS_TRUE;
}

JSBool ngx_javascript_line_string(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	JSString *str;
	const char *file;
	int line;
	if (ngx_javascript_file_line(jcx, &file, &line) == JS_TRUE) {
		str = JS_ValueToString(jcx, INT_TO_JSVAL(line));
		if (str != NULL) {
			*rval = STRING_TO_JSVAL(str);
		}
	}
	return JS_TRUE;
}

/*
 *
 */
u_char *ngx_javascript_string_token(u_char *s, const u_char *delim, u_char ** last) {
	u_char *spanp;
	int c, sc;
	u_char *tok;
	if (s == NULL && (s = *last) == NULL) {
		return NULL;
	}
	cont:
	c = *s ++;
	for (spanp = (u_char *) delim; (sc = *spanp ++) != 0; ) {
		if (c == sc) {
			goto cont;
		}
	}
	if (c == 0) {
		*last = NULL;
		return NULL;
	}
	tok = s - 1;
	for (;;) {
		c = *s ++;
		spanp = (u_char *) delim;
		do {
			if ((sc = *spanp ++) == c) {
				if (c == 0) {
					s = NULL;
				} else {
					u_char *w = s - 1;
					*w = '\0';
				}
				*last = s;
				return tok;
			}
		} while (sc != 0);
	}
}
